// Copyright 2016 Google Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

// stylelint-disable selector-class-pattern --
// Selector '.mdc-*' should only be used in this project.

@use '@material/elevation/elevation-theme';
@use '@material/animation/functions' as animation-functions;
@use '@material/feature-targeting/feature-targeting';
@use '@material/ripple/ripple-theme';
@use '@material/shape/mixins' as shape-mixins;
@use '@material/theme/css';
@use '@material/theme/custom-properties';
@use '@material/theme/keys';
@use '@material/theme/replace';
@use '@material/theme/state';
@use '@material/theme/theme';
@use '@material/theme/theme-color';
@use '@material/tokens/resolvers';
@use './fab-custom-properties';
@use 'sass:math';
@use 'sass:list';
@use 'sass:map';
@use 'sass:meta';

$height: 56px !default;
$mini-height: 40px !default;
$shape-radius: 50% !default;
$ripple-target: '.mdc-fab__ripple';

$light-theme: (
  container-color: secondary,
  container-elevation: 6,
  container-height: 56px,
  container-shadow-color: black,
  container-shape: $shape-radius,
  container-surface-tint-layer-color: null,
  container-width: 56px,
  focus-container-elevation: null,
  focus-icon-color: null,
  focus-outline-color: null,
  focus-outline-width: null,
  focus-state-layer-color: theme-color.$primary,
  focus-state-layer-opacity: null,
  hover-container-elevation: null,
  hover-icon-color: null,
  hover-state-layer-color: theme-color.$primary,
  hover-state-layer-opacity: null,
  icon-color: on-secondary,
  icon-size: 24px,
  lowered-container-elevation: null,
  lowered-focus-container-elevation: null,
  lowered-hover-container-elevation: null,
  lowered-pressed-container-elevation: null,
  pressed-container-elevation: null,
  pressed-icon-color: null,
  pressed-ripple-color: null,
  pressed-ripple-opacity: null,
  pressed-state-layer-color: theme-color.$primary,
  pressed-state-layer-opacity: null,
);

$custom-property-prefix: 'fab';

///
/// Applies the given theme as custom properties without any selectors.
///
@mixin theme($theme, $resolvers: resolvers.$material) {
  @include theme.validate-theme($light-theme, $theme);
  $resolved-theme: resolve-theme($theme, $resolvers);
  @include keys.declare-custom-properties(
    $resolved-theme,
    $prefix: $custom-property-prefix
  );
}

@mixin theme-styles($theme, $resolvers: resolvers.$material) {
  @include theme.validate-theme($light-theme, $theme);

  $theme: keys.create-theme-properties(
    $theme,
    $prefix: $custom-property-prefix
  );
  @include base-theme-styles($theme, $resolvers: $resolvers);

  $shape-radius: map.get($theme, container-shape);
  @if $shape-radius {
    @include shape-radius($shape-radius);
  }
}

///
/// Resolves the given theme with the given resolvers.
///
@function resolve-theme($theme, $resolvers) {
  $elevation-resolver: map.get($resolvers, elevation);
  @return _resolve-theme-elevation-keys(
    $theme,
    $elevation-resolver,
    (
      container-elevation,
      hover-container-elevation,
      focus-container-elevation,
      pressed-container-elevation,
      disabled-container-elevation,
      lowered-container-elevation,
      lowered-focus-container-elevation,
      lowered-hover-container-elevation,
      lowered-pressed-container-elevation
    )
  );
}

///
/// Returns the theme with the elevation keys resolved.
///
@function _resolve-theme-elevation-keys($theme, $resolver, $elevation-keys) {
  @if $resolver == null {
    @return $theme;
  }

  // Shadow color is universal for the component.
  $shadow-color: map.get($theme, container-shadow-color);

  @each $key in $elevation-keys {
    $elevation: map.get($theme, $key);
    @if $elevation != null {
      $resolved-value: meta.call(
        $resolver,
        $elevation: $elevation,
        $shadow-color: $shadow-color
      );
      // Update the key with the resolved value.
      $theme: map.set($theme, $key, $resolved-value);
    }
  }

  @return $theme;
}

@mixin base-theme-styles($theme, $resolvers: resolvers.$material) {
  @include container-color(map.get($theme, container-color));
  @include _container-elevation(
    map.get($resolvers, elevation),
    map.get($theme, container-shadow-color),
    map.get($theme, container-surface-tint-layer-color),
    (
      default: map.get($theme, container-elevation),
      hover: map.get($theme, hover-container-elevation),
      focus: map.get($theme, focus-container-elevation),
      pressed: map.get($theme, pressed-container-elevation),
    )
  );
  @include _container-height(map.get($theme, container-height));
  @include _container-width(map.get($theme, container-width));
  @include icon-size(map.get($theme, icon-size));
  @include _icon-color(
    (
      default: map.get($theme, icon-color),
      hover: map.get($theme, hover-icon-color),
      focus: map.get($theme, focus-icon-color),
      pressed: map.get($theme, pressed-icon-color),
    )
  );

  $opacity-map: (
    hover: map.get($theme, hover-state-layer-opacity),
    focus: map.get($theme, focus-state-layer-opacity),
    press: map.get($theme, pressed-state-layer-opacity),
  );
  $hover-state-layer-color: map.get($theme, hover-state-layer-color);
  @if $hover-state-layer-color {
    @include ripple-color($hover-state-layer-color, $opacity-map: $opacity-map);
  }

  $focus-outline-color: map.get($theme, focus-outline-color);
  @if $focus-outline-color {
    @include focus-outline-color($focus-outline-color);
  }
  $focus-outline-width: map.get($theme, focus-outline-width);
  @if $focus-outline-width {
    @include focus-outline-width($focus-outline-width);
  }
}

@mixin ripple-color($color, $opacity-map: (), $query: feature-targeting.all()) {
  @include ripple-theme.states(
    $color,
    $opacity-map: $opacity-map,
    $query: $query,
    $ripple-target: $ripple-target
  );
}

@mixin accessible($container-color, $query: feature-targeting.all()) {
  @include container-color($container-color, $query: $query);

  $fill-tone: theme-color.tone($container-color);

  @if ($fill-tone == 'dark') {
    @include ink-color(text-primary-on-dark, $query: $query);
    @include ripple-theme.states(
      text-primary-on-dark,
      $query: $query,
      $ripple-target: $ripple-target
    );
  } @else {
    @include ink-color(text-primary-on-light, $query: $query);
    @include ripple-theme.states(
      text-primary-on-light,
      $query: $query,
      $ripple-target: $ripple-target
    );
  }
}

@mixin container-color($color, $query: feature-targeting.all()) {
  $feat-color: feature-targeting.create-target($query, color);

  @include feature-targeting.targets($feat-color) {
    @include theme.property(background-color, $color);
  }
}

@mixin icon-size($width, $height: $width, $query: feature-targeting.all()) {
  $feat-structure: feature-targeting.create-target($query, structure);

  .mdc-fab__icon {
    @include feature-targeting.targets($feat-structure) {
      @include theme.property('width', $width);
      @include theme.property('height', $height);
      @include theme.property('font-size', $height);
    }
  }
}

@mixin ink-color($color, $query: feature-targeting.all()) {
  $feat-color: feature-targeting.create-target($query, color);

  @include feature-targeting.targets($feat-color) {
    &,
    &:not(:disabled) .mdc-fab__icon,
    &:not(:disabled) .mdc-fab__label,
    &:disabled .mdc-fab__icon,
    &:disabled .mdc-fab__label {
      @include theme.property(color, $color);
    }
  }
}

@mixin _container-height($height) {
  @include theme.property('height', $height);
}

@mixin _container-width($width) {
  @include theme.property('width', $width);
}

@mixin _icon-color($color-or-map) {
  &:not(:disabled) {
    @include _set-icon-color(state.get-default-state($color-or-map));

    &:hover {
      @include _set-icon-color(state.get-hover-state($color-or-map));
    }

    &:focus {
      @include _set-icon-color(state.get-focus-state($color-or-map));
    }

    &:active {
      @include _set-icon-color(state.get-pressed-state($color-or-map));
    }
  }

  &:disabled {
    @include _set-icon-color(state.get-disabled-state($color-or-map));
  }
}

@mixin _set-icon-color($color) {
  .mdc-fab__icon {
    @include theme.property(color, $color);
  }
}

@mixin _container-elevation($resolver, $shadow-color, $container-color, $map) {
  &:not(:disabled) {
    @include elevation-theme.with-resolver(
      $resolver,
      $elevation: state.get-default-state($map),
      $shadow-color: $shadow-color
    );
    @include elevation-theme.overlay-container-color($container-color);

    &:hover {
      @include elevation-theme.with-resolver(
        $resolver,
        $elevation: state.get-hover-state($map),
        $shadow-color: $shadow-color
      );
      @include elevation-theme.overlay-container-color($container-color);
    }

    &:focus {
      @include elevation-theme.with-resolver(
        $resolver,
        $elevation: state.get-focus-state($map),
        $shadow-color: $shadow-color
      );
      @include elevation-theme.overlay-container-color($container-color);
    }

    &:active {
      @include elevation-theme.with-resolver(
        $resolver,
        $elevation: state.get-pressed-state($map),
        $shadow-color: $shadow-color
      );
      @include elevation-theme.overlay-container-color($container-color);
    }
  }

  &:disabled {
    // FAB does not have disabled state. Use default state's elevation.
    @include elevation-theme.with-resolver(
      $resolver,
      $elevation: state.get-default-state($map),
      $shadow-color: $shadow-color
    );
  }
}

///
/// Sets outline width only when button is in focus. Also sets padding to
/// include outline on focus (Helps prevent size jump on focus).
/// @param {Number} $width - Outline (border) width.
/// @param {Number|List} $padding [0] - Padding when button is not in focus.
///     Offsets padding based on given outline width on focus.
///
@mixin focus-outline-width(
  $width,
  $padding: 0,
  $query: feature-targeting.all()
) {
  $feat-structure: feature-targeting.create-target($query, structure);

  $padding: css.unpack-value($padding);
  $padding-fallbacks: (0 0 0 0);
  $is-padding-custom-prop: (false false false false);
  $is-width-custom-prop: custom-properties.is-custom-prop($width);
  $width-fallback: if(
    custom-properties.is-custom-prop($width),
    custom-properties.get-fallback($width),
    $width
  );
  $width: if(
    custom-properties.is-custom-prop($width),
    custom-properties.get-declaration-value($width),
    $width
  );

  // conform padding values and extract custom property metadata from them
  @for $i from 1 through list.length($padding) {
    $value: list.nth($padding, $i);
    $value-is-custom-prop: custom-properties.is-custom-prop($value);
    // css max will fail to compare a bare 0 to a px value
    $value: if($value == 0, 0px, $value);
    $value-fallback: if(
      $value-is-custom-prop,
      custom-properties.get-fallback($value),
      $value
    );
    $value: if(
      $value-is-custom-prop,
      custom-properties.get-declaration-value($value),
      $value
    );

    $padding: list.set-nth($padding, $i, $value);
    $padding-fallbacks: list.set-nth($padding-fallbacks, $i, $value-fallback);
    $is-padding-custom-prop: list.set-nth(
      $is-padding-custom-prop,
      $i,
      $value-is-custom-prop
    );
  }

  // Padding should include outline width which will be set on focus.
  // sass math required for IE since IE doesn't support css max
  $padding-top-fallback: math.max(
    list.nth($padding-fallbacks, 1),
    $width-fallback
  );
  $padding-right-fallback: math.max(
    list.nth($padding-fallbacks, 2),
    $width-fallback
  );
  $padding-bottom-fallback: math.max(
    list.nth($padding-fallbacks, 3),
    $width-fallback
  );
  $padding-left-fallback: math.max(
    list.nth($padding-fallbacks, 4),
    $width-fallback
  );

  $padding-top: replace.replace-string(
    'max(paddingval, width)',
    (
      paddingval: list.nth($padding, 1),
      width: $width,
    )
  );
  $padding-right: replace.replace-string(
    'max(paddingval, width)',
    (
      paddingval: list.nth($padding, 2),
      width: $width,
    )
  );
  $padding-bottom: replace.replace-string(
    'max(paddingval, width)',
    (
      paddingval: list.nth($padding, 3),
      width: $width,
    )
  );
  $padding-left: replace.replace-string(
    'max(paddingval, width)',
    (
      paddingval: list.nth($padding, 4),
      width: $width,
    )
  );

  $top-has-custom-prop: $is-width-custom-prop or
    list.nth($is-padding-custom-prop, 1);
  @include css.declaration(padding-top, $padding-top-fallback);
  @if $top-has-custom-prop {
    @include css.declaration(
      padding-top,
      $padding-top,
      $gss: (alternate: $top-has-custom-prop)
    );
  }

  $right-has-custom-prop: $is-width-custom-prop or
    list.nth($is-padding-custom-prop, 2);
  @include css.declaration(padding-right, $padding-right-fallback);
  @if $right-has-custom-prop {
    @include css.declaration(
      padding-right,
      $padding-right,
      $gss: (alternate: $right-has-custom-prop)
    );
  }

  $bottom-has-custom-prop: $is-width-custom-prop or
    list.nth($is-padding-custom-prop, 3);
  @include css.declaration(padding-bottom, $padding-bottom-fallback);
  @if $bottom-has-custom-prop {
    @include css.declaration(
      padding-bottom,
      $padding-bottom,
      $gss: (alternate: $bottom-has-custom-prop)
    );
  }

  $left-has-custom-prop: $is-width-custom-prop or
    list.nth($is-padding-custom-prop, 4);
  @include css.declaration(padding-left, $padding-left-fallback);
  @if $left-has-custom-prop {
    @include css.declaration(
      padding-left,
      $padding-left,
      $gss: (alternate: $left-has-custom-prop)
    );
  }

  &:not(:disabled) {
    @include ripple-theme.focus() {
      @include feature-targeting.targets($feat-structure) {
        border-style: solid;
        @include theme.property(border-width, $width);

        // sass math required for IE since IE doesn't support css max
        $padding-top-fallback: math.abs(
          list.nth($padding-fallbacks, 1) - $width-fallback
        );
        $padding-right-fallback: math.abs(
          list.nth($padding-fallbacks, 2) - $width-fallback
        );
        $padding-bottom-fallback: math.abs(
          list.nth($padding-fallbacks, 3) - $width-fallback
        );
        $padding-left-fallback: math.abs(
          list.nth($padding-fallbacks, 4) - $width-fallback
        );

        // max(a, calc(a * -1)) is equivalent to math.abs
        $padding-top: replace.replace-string(
          'max(paddingcalc, calc(paddingcalc * -1))',
          (
            paddingcalc: 'calc(paddingval - width)',
            paddingval: list.nth($padding, 1),
            width: $width,
          )
        );
        $padding-right: replace.replace-string(
          'max(paddingcalc, calc(paddingcalc * -1))',
          (
            paddingcalc: 'calc(paddingval - width)',
            paddingval: list.nth($padding, 2),
            width: $width,
          )
        );
        $padding-bottom: replace.replace-string(
          'max(paddingcalc, calc(paddingcalc * -1))',
          (
            paddingcalc: 'calc(paddingval - width)',
            paddingval: list.nth($padding, 3),
            width: $width,
          )
        );
        $padding-left: replace.replace-string(
          'max(paddingcalc, calc(paddingcalc * -1))',
          (
            paddingcalc: 'calc(paddingval - width)',
            paddingval: list.nth($padding, 4),
            width: $width,
          )
        );

        @include css.declaration(padding-top, $padding-top-fallback);
        @if $top-has-custom-prop {
          @include css.declaration(
            padding-top,
            $padding-top,
            $gss: (alternate: $top-has-custom-prop)
          );
        }
        @include css.declaration(padding-right, $padding-right-fallback);
        @if $right-has-custom-prop {
          @include css.declaration(
            padding-right,
            $padding-right,
            $gss: (alternate: $right-has-custom-prop)
          );
        }
        @include css.declaration(padding-bottom, $padding-bottom-fallback);
        @if $bottom-has-custom-prop {
          @include css.declaration(
            padding-bottom,
            $padding-bottom,
            $gss: (alternate: $bottom-has-custom-prop)
          );
        }
        @include css.declaration(padding-left, $padding-left-fallback);
        @if $left-has-custom-prop {
          @include css.declaration(
            padding-left,
            $padding-left,
            $gss: (alternate: $left-has-custom-prop)
          );
        }
      }
    }
  }
}

///
/// Sets outline color only when button is in focus. Use `focus-outline-width()`
/// to set appropriate outline width.
/// @param {Color} $color - Outline (border) color.
///
@mixin focus-outline-color($color, $query: feature-targeting.all()) {
  $feat-color: feature-targeting.create-target($query, color);

  &:not(:disabled) {
    @include ripple-theme.focus() {
      @include feature-targeting.targets($feat-color) {
        @include theme.property(border-color, $color);
      }
    }
  }
}

@mixin shape-radius(
  $radius,
  $rtl-reflexive: false,
  $query: feature-targeting.all()
) {
  &:not(.mdc-fab--extended) {
    // Do not specify $component-height for shape radius. FABs are circular,
    // which means they can use percentage border radius without resolving to
    // a component height.
    @include shape-mixins.radius($radius, $rtl-reflexive, $query: $query);

    #{$ripple-target} {
      @include shape-mixins.radius($radius, $rtl-reflexive, $query: $query);
    }
  }
}
